/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import ts from 'typescript';
import { FileUtil } from '@ohos/hvigor';
import { HMRouterPluginConfig } from './HMRouterPluginConfig';
import { AnalyzerResultLike, HMRouterResult } from './common/PluginModel';
import { Logger, PluginError } from './common/Logger';
import { HMRouterPluginConstant } from './common/Constant';

class NodeInfo {
  value?: any;
}

export class Analyzer {
  analyzeResultSet: Set<AnalyzerResultLike> = new Set();
  private readonly sourcePath: string;
  private readonly pluginConfig: HMRouterPluginConfig;
  private analyzeResult: AnalyzerResultLike = {};
  private classMethodAnalyzeResult: Set<AnalyzerResultLike> = new Set();
  private keywordPos: number = 0;
  private importMap: Map<string, string[]> = new Map();

  constructor(sourcePath: string, pluginConfig: HMRouterPluginConfig) {
    this.sourcePath = sourcePath;
    this.pluginConfig = pluginConfig;
  }

  start() {
    const sourceCode = FileUtil.readFileSync(this.sourcePath).toString();
    const sourceFile = ts.createSourceFile(this.sourcePath, sourceCode, ts.ScriptTarget.ES2021, false);
    ts.forEachChild(sourceFile, node => {
      this.resolveNode(node);
      if (this.analyzeResult.annotation) {
        this.analyzeResultSet.add(this.analyzeResult);
      }
      if (this.classMethodAnalyzeResult.size > 0) {
        this.analyzeResultSet = new Set([...this.analyzeResultSet, ...this.classMethodAnalyzeResult]);
      }
    });
    // 遍历map查看是否还有常量类型的属性值
    this.analyzeResultSet.forEach(value => {
      if (value.annotation === 'HMRouter') {
        let hmRouterResult = value as HMRouterResult;
        if (hmRouterResult.pageUrl.type === 'constant') {
          hmRouterResult.pageUrl = this.resolveConstantValue(hmRouterResult.pageUrl.value);
        } else if (hmRouterResult.pageUrl.type === 'object') {
          hmRouterResult.pageUrl =
            this.resolveObjectValue(hmRouterResult.pageUrl.className, hmRouterResult.pageUrl.propertyName);
        }
      }
    });
  }

  private resolveNode(node: ts.Node) {
    if (ts.isImportDeclaration(node)) {
      this.resolveImportDeclaration(node);
    } else if (ts.isMissingDeclaration(node)) {
      this.resolveMissingDeclaration(node);
    } else if (ts.isClassDeclaration(node)) {
      this.isDecoratedClass(node) ? this.resolveClass(node) : this.resolveClassMethod(node);
    } else if (ts.isDecorator(node)) {
      this.resolveDecorator(node);
    } else if (ts.isCallExpression(node)) {
      this.resolveCallExpression(node);
    } else if (ts.isExpressionStatement(node)) {
      this.resolveExpression(node);
    } else if (ts.isBlock(node)) {
      this.resolveBlock(node);
    } else if (ts.isPropertyAssignment(node)) {
      return this.resolvePropertyAccess(node);
    } else if (ts.isIdentifier(node)) {
      return this.resolveIdentifier(node);
    } else if (ts.isStringLiteral(node)) {
      return this.resolveStringLiteral(node);
    } else if (ts.isNumericLiteral(node)) {
      return this.resolveNumericLiteral(node);
    } else if (ts.isArrayLiteralExpression(node)) {
      return this.resolveArrayLiteral(node);
    } else if (node.kind === ts.SyntaxKind.TrueKeyword) {
      return this.resolveTrueKeyword();
    } else if (node.kind === ts.SyntaxKind.FalseKeyword) {
      return this.resolveFalseKeyword();
    }
  }

  private resolveImportDeclaration(node: ts.ImportDeclaration) {
    let variableArr: string[] = [];
    node.importClause?.namedBindings?.forEachChild(child => {
      if (ts.isImportSpecifier(child)) {
        variableArr.push(child.name.escapedText!);
      }
    });
    variableArr.push(node.importClause?.name?.text!);
    let key = node.moduleSpecifier;
    if (ts.isModuleName(key)) {
      this.importMap.set(key.text, variableArr);
    }
  }

  private resolveMissingDeclaration(node: ts.MissingDeclaration) {
    this.analyzeResult = {};
    node.forEachChild(child => this.resolveNode(child));
  }

  private isDecoratedClass(node: ts.ClassDeclaration) {
    return node.modifiers?.some(
      (item: any) => HMRouterPluginConstant.CLASS_ANNOTATION_ARR.includes(item.expression?.expression?.text));
  }

  private resolveClass(node: ts.ClassDeclaration) {
    // 解析到类声明，先清空一次返回结果
    this.analyzeResult = {};
    node.modifiers?.forEach(modifier => {
      // 遍历分析装饰器
      this.resolveNode(modifier);
    });
    if (this.analyzeResult.annotation) {
      this.analyzeResult.name = node.name?.text;
    }
  }

  private resolveClassMethod(node: ts.ClassDeclaration) {
    this.classMethodAnalyzeResult = new Set([]);
    node.members?.forEach((member) => {
      this.analyzeResult = {};
      if (ts.isMethodDeclaration(member) && member.modifiers) {
        member.modifiers?.forEach((modifier) => {
          this.resolveNode(modifier);
        });
        // @ts-ignore
        this.analyzeResult.functionName = member.name?.text;
        this.analyzeResult.name = node.name?.text;
        if (this.analyzeResult.annotation) {
          this.classMethodAnalyzeResult.add(this.analyzeResult);
        }
        this.analyzeResult = {};
      }
    });
  }

  private resolveDecorator(node: ts.Decorator) {
    if (ts.isCallExpression(node.expression)) {
      const callExpression = node.expression as ts.CallExpression;
      if (ts.isIdentifier(callExpression.expression)) {
        this.switchIdentifier(callExpression);
      }
    }
  }

  private switchIdentifier(callExpression: ts.CallExpression) {
    const identifier = callExpression.expression as ts.Identifier;
    if (this.pluginConfig.annotation.some(item => item === identifier.text)) {
      this.analyzeResult.annotation = identifier.text;
      this.resolveNode(callExpression);
    }
  }

  private resolveCallExpression(node: ts.CallExpression) {
    let identifier = this.resolveNode(node.expression);
    this.parseAnnotation(node.arguments, identifier);
  }

  private resolveExpression(node: ts.ExpressionStatement) {
    let identifier = this.resolveNode(node.expression);
    if (identifier?.value === 'struct') {
      this.keywordPos = node.end;
    }
    if (this.analyzeResult.annotation === 'HMRouter' && this.keywordPos === node.pos) {
      this.analyzeResult.name = identifier?.value;
    }
  }

  private resolveBlock(node: ts.Block) {
    node.statements.forEach(statement => {
      this.resolveNode(statement);
    });
  }

  private parseAnnotation(args: ts.NodeArray<ts.Expression>, nodeInfo?: NodeInfo) {
    if (this.pluginConfig.annotation.some(item => nodeInfo?.value === item)) {
      args
        .flatMap((e: ts.Expression) => (e as ts.ObjectLiteralExpression).properties)
        .forEach((e: ts.ObjectLiteralElementLike) => {
          this.parseConfig(e, this.analyzeResult);
        });
    }
  }

  private parseConfig(node: ts.ObjectLiteralElementLike, result: AnalyzerResultLike) {
    let info = this.resolveNode(node);
    Reflect.set(result, info?.value['key'], info?.value['value']);
  }

  private resolvePropertyAccess(node: any) {
    let propertyName = this.resolveNode(node.name)?.value;
    let propertyValue;
    if (propertyName === 'pageUrl') {
      if (node.initializer.kind === ts.SyntaxKind.Identifier) {
        propertyValue = {
          type: 'constant', // 常量
          value: this.resolveNode(node.initializer)?.value
        };
      } else if (node.initializer.kind === ts.SyntaxKind.PropertyAccessExpression) {
        propertyValue = {
          type: 'object', // 对象
          className: this.resolveNode(node.initializer.expression)?.value,
          propertyName: this.resolveNode(node.initializer.name)?.value
        };
      } else {
        propertyValue = this.resolveNode(node.initializer)?.value;
      }
    } else {
      propertyValue = this.resolveNode(node.initializer)?.value;
    }
    let info = new NodeInfo();
    info.value = {key: propertyName, value: propertyValue};
    return info;
  }

  private resolveIdentifier(node: ts.Identifier) {
    if (node.escapedText === 'NavDestination' && this.analyzeResult.annotation === 'HMRouter') {
      Logger.error(PluginError.ERR_WRONG_DECORATION);
      throw new Error('NavDestination is not allowed in HMRouter' + this.sourcePath);
    }
    let info = new NodeInfo();
    info.value = node.escapedText.toString();
    return info;
  }

  private resolveArrayLiteral(node: ts.ArrayLiteralExpression) {
    let arrValue = node.elements.map(e => this.resolveNode(e)?.value as string);
    let info = new NodeInfo();
    info.value = arrValue;
    return info;
  }

  private resolveNumericLiteral(node: ts.NumericLiteral) {
    let info = new NodeInfo();
    info.value = Number(node.text);
    return info;
  }

  private resolveStringLiteral(node: ts.StringLiteral) {
    let info = new NodeInfo();
    info.value = node.text;
    return info;
  }

  private resolveTrueKeyword() {
    let info = new NodeInfo();
    info.value = true;
    return info;
  }

  private resolveFalseKeyword() {
    let info = new NodeInfo();
    info.value = false;
    return info;
  }

  private getSourceFile(filePath: string): ts.SourceFile | null {
    if (!FileUtil.exist(filePath)) {
      return null;
    }
    const sourceCode = FileUtil.readFileSync(filePath).toString();
    return ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.ES2021, false);
  }

  private findConstantValueInSourceFile(sourceFile: ts.SourceFile, constantName: string): NodeInfo {
    let nodeInfo = new NodeInfo();
    ts.forEachChild(sourceFile, node => {
      if (!ts.isVariableStatement(node)) {
        return;
      }
      node.declarationList.declarations.forEach(declaration => {
        let info = this.parseVariableDeclaration(declaration);
        if (constantName === info?.value['key']) {
          nodeInfo = info;
        }
      });
    });
    return nodeInfo;
  }

  private findObjectValueInSourceFile(sourceFile: ts.SourceFile, className: string,
    propertyName: string): NodeInfo {
    let info = new NodeInfo();
    ts.forEachChild(sourceFile, node => {
      if (!ts.isClassDeclaration(node) || node.name?.escapedText.toString() !== className) {
        return;
      }
      node.members.forEach(member => {
        // @ts-ignore
        if (member.name?.escapedText.toString() === propertyName) {
          info = this.parseVariableDeclaration(member);
        }
      });
    });
    return info;
  }

  private resolveConstantValue(constantName: string) {
    const sourceFile = this.getSourceFile(this.resolvePath(constantName));
    if (!sourceFile) {
      return null;
    }
    const constantValueStr = this.findConstantValueInSourceFile(sourceFile, constantName).value['value'];
    if (!constantValueStr) {
      Logger.error(PluginError.ERR_NOT_EMPTY_STRING, constantName);
      throw new Error('constant value cannot be an empty string');
    }
    return constantValueStr;
  }

  private resolveObjectValue(className: string, propertyName: string) {
    const sourceFile = this.getSourceFile(this.resolvePath(className));
    if (!sourceFile) {
      return null;
    }
    const objectValueStr = this.findObjectValueInSourceFile(sourceFile, className, propertyName).value['value'];
    if (!objectValueStr) {
      Logger.error(PluginError.ERR_NOT_EMPTY_STRING, propertyName);
      throw new Error('constant value cannot be an empty string');
    }
    return objectValueStr;
  }


  private resolvePath(variableName: any) {
    let filePath: string = '';
    this.importMap.forEach((value, key) => {
      if (value.includes(variableName)) {
        filePath = key;
      }
    });
    if (filePath) {
      filePath =
        FileUtil.pathResolve(
          this.sourcePath.split(HMRouterPluginConstant.FILE_SEPARATOR).slice(0, -1)
            .join(HMRouterPluginConstant.FILE_SEPARATOR),
          filePath
        ) + HMRouterPluginConstant.VIEW_NAME_SUFFIX;
    } else {
      filePath = this.sourcePath;
    }
    return filePath;
  }

  private parseVariableDeclaration(node: any) {
    let variableName = this.resolveNode(node.name)?.value;
    let variableValue = this.resolveNode(node.initializer!)?.value;
    if (typeof variableValue !== 'string') {
      Logger.error(PluginError.ERR_INVALID_STRING_VALUE, variableName);
      throw new Error('Invalid constant value');
    }
    let info = new NodeInfo();
    info.value = {key: variableName, value: variableValue};
    return info;
  }
}
